package com.hmdp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.LogicExpireDTO;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IShopService;
import com.hmdp.utils.RedisClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.SystemConstants;
import io.netty.util.internal.StringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.time.temporal.TemporalUnit;
import java.util.*;
import java.util.concurrent.*;

import static com.hmdp.utils.RedisConstants.SHOP_GEO_KEY;
import static com.hmdp.utils.SystemConstants.DEFAULT_PAGE_SIZE;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Autowired
//           这是一个组件，当然要去注入
            RedisClient redisClient;
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Override
    public Result iGetById(Long id) {
        //常规查询
        //return getByIdNormal(id);

        //缓存穿透
        //return cachePenetration(id);

        //缓存击穿使用互斥锁
        //return cacheBreakdownWithMutex(id);

        //缓存击穿使用逻辑过期
        return cacheBreakdownWithLogic(id);

        //缓存穿透工具类,
//        Shop shop = redisClient.cachePenetration(id, RedisConstants.CACHE_SHOP_KEY, Shop.class, this::getById);
//        if(shop==null) return Result.fail("店铺不存在");
//        return Result.ok(shop);

        //逻辑过期工具咧
//        Shop shop = redisClient.cacheBreakdownWithLogic(id, RedisConstants.CACHE_SHOP_KEY, Shop.class, this::getById,RedisConstants.LOCK_SHOP_KEY,RedisConstants.CACHE_SHOP_TTL,TimeUnit.SECONDS);
//        if(shop==null) return Result.fail("店铺不存在");
//        return Result.ok(shop);
    }

    //    使用的是ExecutorService
    private static ExecutorService POOL_EXECUTOR = Executors.newSingleThreadExecutor();

    /**
     * @Description: 互斥锁，逻辑过期
     * @Param: * @param id
     * @return: com.hmdp.dto.Result
     * @Author: Feng
     * @Date:2023/03/09 6:16
     */
    private Result cacheBreakdownWithLogic(Long id) {
        //从缓存中查询
        String dtoJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        //用函数来判断，
//        如果为空的话就直接返回null，表示不是热点数据
        if (StrUtil.isBlank(dtoJson)) {
            return null;
        }

        //使用json对字符串反序列化
        LogicExpireDTO dto = JSONUtil.toBean(dtoJson, LogicExpireDTO.class);
        LocalDateTime expire = dto.getExpire();
//        json转成bean，取这个bean的object类型的属性需要转成JSONObject
//        由于不知道data的类型会转成JSONObject
        JSONObject data = (JSONObject) dto.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);

        if (expire.isAfter(LocalDateTime.now())) return Result.ok(shop);


        String lock = RedisConstants.LOCK_SHOP_KEY + id;

        //获取不到锁，直接返回旧数据
        if (!tryLock(lock)) {
            return Result.ok(shop);
        }
        //双重锁检验
        dtoJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        if (StrUtil.isBlank(dtoJson)) {
            return null;
        }
        dto = JSONUtil.toBean(dtoJson, LogicExpireDTO.class);
        expire = dto.getExpire();
        data = (JSONObject) dto.getData();
        shop = JSONUtil.toBean(data, Shop.class);
        if (expire.isAfter(LocalDateTime.now())) return Result.ok(shop);


        //开启线程进行缓存重建，返回旧数据
        POOL_EXECUTOR.submit(() -> {
            try {
                Thread.sleep(200);
//                    直接建预热数据就行了，其中有查询数据库的语句
                addPreHeadtingData(id, RedisConstants.CACHE_SHOP_KEY, 30L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
//                重建完再释放锁
//                注意数据库查询完了才能释放缓存
                releaseLock(lock);
            }

        });


        return Result.ok(shop);
    }


    /**
     * @param prefix
     * @param expireSecond
     * @Description: 添加预热数据
     * @Param: * @param id
     * @return: void
     * @Author: Feng
     * @Date:2023/03/09 6:15
     */
    public void addPreHeadtingData(Long id, String prefix, Long expireSecond) {
//        直接外面传来id和时间和前缀
        Shop shop = query().eq("id", id).one();
//        这个dto的过期时间就直接使用LocalDateTime
        LogicExpireDTO dto = new LogicExpireDTO(shop, LocalDateTime.now().plusSeconds(expireSecond));
        stringRedisTemplate.opsForValue().set(prefix + id, JSONUtil.toJsonStr(dto));
    }

    /**
     * @Description: 缓存击穿（互斥锁）
     * @Param: * @param id
     * @return: com.hmdp.dto.Result
     * @Author: Feng
     * @Date:2023/03/08 16:24
     */
    private Result cacheBreakdownWithMutex(Long id) {
        //从缓存中查询
        String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        //用函数来判断，
        if (StrUtil.isNotBlank(shopJson)) {
            //使用json对字符串反序列化
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //对""进行判断,直接返回null
        if (shopJson != null) return null;
        Shop shop = null;

        String lock = RedisConstants.LOCK_SHOP_KEY + id;

        try {
            //获取锁，进行数据库查询
            if (!tryLock(lock)) {
                Thread.sleep(200);
                //递归查询缓存
                cacheBreakdownWithMutex(id);
            }
            //双重锁检验
            shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
            if (StrUtil.isNotBlank(shopJson)) {
                shop = JSONUtil.toBean(shopJson, Shop.class);
                return Result.ok(shop);
            }
            if (shopJson != null) return null;

            //不存在，则查询数据库放入缓存
            Thread.sleep(200);
            shop = query().eq("id", id).one();

            if (shop == null) {
                stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }

            stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            releaseLock(lock);
        }

        return Result.ok(shop);
    }

    private void releaseLock(String lock) {
        stringRedisTemplate.delete(lock);
    }

    private boolean tryLock(String lock) {
        return stringRedisTemplate.opsForValue().setIfAbsent(lock, "");
    }

    /**
     * @Description: 缓存穿透
     * @Param: * @param id
     * @return: com.hmdp.dto.Result
     * @Author: Feng
     * @Date:2023/03/08 16:02
     */
    private Result cachePenetration(Long id) {
        //从缓存中查询
        String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
//        用函数来判断，
        if (StrUtil.isNotBlank(shopJson)) {
            //使用json对字符串反序列化
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
//        对""进行判断,直接返回null
        if (shopJson != null) return null;

        //不存在，则查询数据库放入缓存
        Shop shop = query().eq("id", id).one();

        if (shop == null) {
            stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);

        return Result.ok(shop);
    }

    /**
     * @Description: 正常情况下查询
     * @Param: * @param id
     * @return: com.hmdp.dto.Result
     * @Author: Feng
     * @Date:2023/03/08 16:03
     */
    private Result getByIdNormal(Long id) throws InterruptedException {
        //从缓存中查询
        String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
        if (shopJson != null) {
//            使用json对字符串反序列化
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //不存在，则查询数据库放入缓存
        Thread.sleep(200);
        Shop shop = query().eq("id", id).one();
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

    @Override
    @Transactional
    public Result iUpdateById(Shop shop) {
//        需要判断id不为null
        if (shop.getId() == null) return Result.fail("shop的id不能为null");
        boolean b = updateById(shop);
        stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shop.getId());

        if (b) return Result.ok();
        return Result.fail("更新失败");
    }

    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
//        x=140.89;y=80.21;
        if (x == null || y == null) {
            // 根据类型分页查询
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page.getRecords());
        }
//        需要分页参数
        int from = (current-1)*DEFAULT_PAGE_SIZE;
        int to = current*DEFAULT_PAGE_SIZE;
        //根据typeid查询redis,根据距离，返回值要带上距离
//        limit,从0开始到第几页的
//        带上距离
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = stringRedisTemplate.opsForGeo()
                .search(
                SHOP_GEO_KEY + typeId,
                GeoReference.fromCoordinate(x,y),
                new Distance(1000*100),
                RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(to));
        //非空判断
        if (geoResults==null) return Result.ok(Collections.emptyList());
//        content中有id，和距离
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = geoResults.getContent();
//        没有这一页了
        if(list.size()<=from) return Result.ok(Collections.emptyList());
        //遍历将数据存储
        List<String> ids = new ArrayList<>();
        Map<String,Double> idDisMap = new HashMap<>();
        list.stream().skip(from).forEach(result->{
            String id = result.getContent().getName();
            ids.add(id);
            idDisMap.put(id,result.getDistance().getValue());
        });
        //根据ids查询shop; order by field(id,3,1);
        String strIds = StrUtil.join(",",ids);
        List<Shop> shopList = query().in("id", ids).last("order by field(id,"+strIds+")").list();
        //给shop赋上距离
        for (Shop shop : shopList) {
            shop.setDistance(idDisMap.get(shop.getId().toString()));
        }

        return Result.ok(shopList);
    }
}
